@objectstack/runtime 6.5.1 → 6.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -2141,6 +2141,7 @@ __export(index_exports, {
2141
2141
  resolveDefaultArtifactPath: () => resolveDefaultArtifactPath,
2142
2142
  resolveErrorReporter: () => resolveErrorReporter,
2143
2143
  resolveMetrics: () => resolveMetrics,
2144
+ resolveObjectStackHome: () => resolveObjectStackHome,
2144
2145
  resolveRequestId: () => resolveRequestId,
2145
2146
  seedPlatformSsoClient: () => seedPlatformSsoClient
2146
2147
  });
@@ -2196,8 +2197,17 @@ var Runtime = class {
2196
2197
  // src/standalone-stack.ts
2197
2198
  var import_node_path2 = require("path");
2198
2199
  var import_node_fs = require("fs");
2200
+ var import_node_os = require("os");
2199
2201
  var import_zod = require("zod");
2200
2202
  init_load_artifact_bundle();
2203
+ function resolveObjectStackHome() {
2204
+ const raw = process.env.OS_HOME?.trim();
2205
+ if (raw && raw.length > 0) {
2206
+ if (raw.startsWith("~")) return (0, import_node_path2.resolve)((0, import_node_os.homedir)(), raw.slice(1).replace(/^[/\\]/, ""));
2207
+ return (0, import_node_path2.resolve)(raw);
2208
+ }
2209
+ return (0, import_node_path2.resolve)((0, import_node_os.homedir)(), ".objectstack");
2210
+ }
2201
2211
  var StandaloneStackConfigSchema = import_zod.z.object({
2202
2212
  databaseUrl: import_zod.z.string().optional(),
2203
2213
  databaseAuthToken: import_zod.z.string().optional(),
@@ -2228,7 +2238,7 @@ async function createStandaloneStack(config) {
2228
2238
  const environmentId = cfg.environmentId ?? process.env.OS_ENVIRONMENT_ID ?? "proj_local";
2229
2239
  const artifactPathInput = cfg.artifactPath ?? process.env.OS_ARTIFACT_PATH ?? (0, import_node_path2.resolve)(cwd, "dist/objectstack.json");
2230
2240
  const artifactPath = isHttpUrl(artifactPathInput) ? artifactPathInput : artifactPathInput.startsWith("/") ? artifactPathInput : (0, import_node_path2.resolve)(cwd, artifactPathInput);
2231
- const dbUrl = cfg.databaseUrl ?? process.env.OS_DATABASE_URL?.trim() ?? process.env.TURSO_DATABASE_URL?.trim() ?? `file:${(0, import_node_path2.resolve)(cwd, ".objectstack/data/standalone.db")}`;
2241
+ const dbUrl = cfg.databaseUrl ?? process.env.OS_DATABASE_URL?.trim() ?? process.env.TURSO_DATABASE_URL?.trim() ?? `file:${(0, import_node_path2.resolve)(resolveObjectStackHome(), "data/standalone.db")}`;
2232
2242
  const dbAuthToken = cfg.databaseAuthToken ?? process.env.OS_DATABASE_AUTH_TOKEN?.trim() ?? process.env.TURSO_AUTH_TOKEN?.trim();
2233
2243
  const explicitDriver = cfg.databaseDriver ?? process.env.OS_DATABASE_DRIVER?.trim();
2234
2244
  const dbDriver = explicitDriver ?? detectDriverFromUrl(dbUrl);
@@ -2237,7 +2247,14 @@ async function createStandaloneStack(config) {
2237
2247
  const { InMemoryDriver } = await import("@objectstack/driver-memory");
2238
2248
  driverPlugin = new DriverPlugin2(new InMemoryDriver());
2239
2249
  } else if (dbDriver === "turso") {
2240
- const { TursoDriver } = await import("@objectstack/driver-turso");
2250
+ let TursoDriver;
2251
+ try {
2252
+ ({ TursoDriver } = await import("@objectstack/driver-turso"));
2253
+ } catch (err) {
2254
+ throw new Error(
2255
+ `[StandaloneStack] libsql/turso URL detected ("${dbUrl}") but @objectstack/driver-turso is not installed. Install it with: npm install @objectstack/driver-turso (or use a file: URL to default to better-sqlite3). (${err?.message ?? err})`
2256
+ );
2257
+ }
2241
2258
  driverPlugin = new DriverPlugin2(
2242
2259
  new TursoDriver({ url: dbUrl, authToken: dbAuthToken })
2243
2260
  );
@@ -2348,12 +2365,42 @@ function resolveDefaultArtifactPath(explicitPath, cwd = process.cwd()) {
2348
2365
  }
2349
2366
  async function createDefaultHostConfig(options = {}) {
2350
2367
  const { requireArtifact = true, ...standaloneOpts } = options;
2351
- const resolvedArtifact = resolveDefaultArtifactPath(standaloneOpts.artifactPath);
2368
+ let resolvedArtifact = resolveDefaultArtifactPath(standaloneOpts.artifactPath);
2352
2369
  if (!resolvedArtifact && requireArtifact) {
2353
2370
  throw new Error(
2354
2371
  "[createDefaultHostConfig] No artifact source available. Set OS_ARTIFACT_PATH (file path or http(s):// URL), place the artifact at <cwd>/dist/objectstack.json, or pass `{ artifactPath: ... }` explicitly. To boot an empty kernel anyway, pass `{ requireArtifact: false }`."
2355
2372
  );
2356
2373
  }
2374
+ if (!resolvedArtifact && !requireArtifact) {
2375
+ const home = resolveObjectStackHome();
2376
+ const stubPath = (0, import_node_path3.resolve)(home, "dist/objectstack.json");
2377
+ if (!(0, import_node_fs2.existsSync)(stubPath)) {
2378
+ (0, import_node_fs2.mkdirSync)((0, import_node_path3.resolve)(stubPath, ".."), { recursive: true });
2379
+ (0, import_node_fs2.writeFileSync)(
2380
+ stubPath,
2381
+ JSON.stringify(
2382
+ {
2383
+ manifest: {
2384
+ id: "com.objectstack.empty",
2385
+ name: "empty",
2386
+ version: "0.0.0",
2387
+ type: "app",
2388
+ description: "Empty starter kernel \u2014 install apps via the Studio marketplace."
2389
+ },
2390
+ objects: [],
2391
+ views: [],
2392
+ apps: [],
2393
+ flows: [],
2394
+ requires: []
2395
+ },
2396
+ null,
2397
+ 2
2398
+ ),
2399
+ "utf8"
2400
+ );
2401
+ }
2402
+ resolvedArtifact = stubPath;
2403
+ }
2357
2404
  return createStandaloneStack({
2358
2405
  ...standaloneOpts,
2359
2406
  artifactPath: resolvedArtifact
@@ -6901,6 +6948,8 @@ var KernelManager = class {
6901
6948
  this.maxSize = config.maxSize ?? 32;
6902
6949
  this.ttlMs = config.ttlMs ?? 15 * 60 * 1e3;
6903
6950
  this.logger = config.logger ?? console;
6951
+ this.freshnessProbe = config.freshnessProbe;
6952
+ this.staleCheckIntervalMs = config.staleCheckIntervalMs ?? 1e4;
6904
6953
  }
6905
6954
  /** Returns the currently cached environmentIds (ordered by insertion). */
6906
6955
  keys() {
@@ -6923,8 +6972,31 @@ var KernelManager = class {
6923
6972
  if (this.ttlMs > 0 && Date.now() - existing.lastAccess > this.ttlMs) {
6924
6973
  await this.evict(environmentId);
6925
6974
  } else {
6926
- existing.lastAccess = Date.now();
6927
- return existing.kernel;
6975
+ if (this.freshnessProbe) {
6976
+ const now = Date.now();
6977
+ if (now - existing.lastStaleCheckAt >= this.staleCheckIntervalMs) {
6978
+ existing.lastStaleCheckAt = now;
6979
+ let stale = false;
6980
+ try {
6981
+ stale = await this.freshnessProbe(environmentId, existing.createdAt);
6982
+ } catch (err) {
6983
+ this.logger.warn?.("[KernelManager] freshness probe failed", { environmentId, err });
6984
+ }
6985
+ if (stale) {
6986
+ this.logger.info?.("[KernelManager] kernel evicted by freshness probe", { environmentId });
6987
+ await this.evict(environmentId);
6988
+ } else {
6989
+ existing.lastAccess = Date.now();
6990
+ return existing.kernel;
6991
+ }
6992
+ } else {
6993
+ existing.lastAccess = Date.now();
6994
+ return existing.kernel;
6995
+ }
6996
+ } else {
6997
+ existing.lastAccess = Date.now();
6998
+ return existing.kernel;
6999
+ }
6928
7000
  }
6929
7001
  }
6930
7002
  const inflight = this.pending.get(environmentId);
@@ -6932,7 +7004,7 @@ var KernelManager = class {
6932
7004
  const promise = (async () => {
6933
7005
  const kernel = await this.factory.create(environmentId);
6934
7006
  const now = Date.now();
6935
- this.cache.set(environmentId, { kernel, createdAt: now, lastAccess: now });
7007
+ this.cache.set(environmentId, { kernel, createdAt: now, lastAccess: now, lastStaleCheckAt: now });
6936
7008
  await this.enforceMaxSize();
6937
7009
  return kernel;
6938
7010
  })();
@@ -7100,6 +7172,30 @@ var ArtifactApiClient = class {
7100
7172
  if (!found?.headCommitId) return null;
7101
7173
  return { commitId: String(found.headCommitId), publishedAt: found.headPublishedAt ?? null };
7102
7174
  }
7175
+ /**
7176
+ * Cheap freshness probe — returns the env's `last_published_at`
7177
+ * (and best-effort current commit) without rebuilding the artifact.
7178
+ * Used by `KernelManager` on cache hits to detect when a per-env
7179
+ * kernel has been invalidated by an upstream change (marketplace
7180
+ * install/uninstall, artifact publish) so it can be rebuilt
7181
+ * without waiting for the 15-minute LRU TTL to expire.
7182
+ *
7183
+ * Returns `null` on definitive 404 / unknown env. Errors propagate
7184
+ * (caller decides whether to treat unreachable cloud as fresh or
7185
+ * stale — typically fresh, so a brief outage doesn't churn every
7186
+ * cached kernel).
7187
+ */
7188
+ async getFreshness(environmentId) {
7189
+ const url = `${this.base}/api/v1/cloud/environments/${encodeURIComponent(environmentId)}/freshness`;
7190
+ const res = await this.request(url);
7191
+ if (res === null) return null;
7192
+ const body = res.success === false ? null : res.data ?? res;
7193
+ if (!body || typeof body !== "object") return null;
7194
+ const envId = typeof body.environmentId === "string" ? body.environmentId : environmentId;
7195
+ const lastPublishedAt = typeof body.lastPublishedAt === "string" ? body.lastPublishedAt : null;
7196
+ const commitId = typeof body.commitId === "string" ? body.commitId : null;
7197
+ return { environmentId: envId, lastPublishedAt, commitId };
7198
+ }
7103
7199
  /** Drop cached entries for a project (and any matching hostname). */
7104
7200
  invalidate(environmentId) {
7105
7201
  this.artifactCache.delete(environmentId);
@@ -7313,7 +7409,14 @@ async function createDriver(driverType, databaseUrl, authToken) {
7313
7409
  }
7314
7410
  case "libsql":
7315
7411
  case "turso": {
7316
- const { TursoDriver } = await import("@objectstack/driver-turso");
7412
+ let TursoDriver;
7413
+ try {
7414
+ ({ TursoDriver } = await import("@objectstack/driver-turso"));
7415
+ } catch (err) {
7416
+ throw new Error(
7417
+ `[ArtifactEnvironmentRegistry] libsql/turso driver requested but @objectstack/driver-turso is not installed. Install it with: npm install @objectstack/driver-turso. (${err?.message ?? err})`
7418
+ );
7419
+ }
7317
7420
  return new TursoDriver({ url: databaseUrl, authToken });
7318
7421
  }
7319
7422
  case "postgres":
@@ -7851,7 +7954,30 @@ var ArtifactKernelFactory = class {
7851
7954
  };
7852
7955
 
7853
7956
  // src/cloud/auth-proxy-plugin.ts
7957
+ var import_node_crypto3 = require("crypto");
7854
7958
  var AUTH_PREFIX = "/api/v1/auth";
7959
+ function signSessionCookieValue(rawToken, secret) {
7960
+ const signature = (0, import_node_crypto3.createHmac)("sha256", secret).update(rawToken).digest("base64");
7961
+ return encodeURIComponent(`${rawToken}.${signature}`);
7962
+ }
7963
+ function buildSetCookieHeader(name, encodedValue, attrs, maxAgeSec) {
7964
+ const parts = [`${name}=${encodedValue}`];
7965
+ const a = attrs ?? {};
7966
+ if (a.path) parts.push(`Path=${a.path}`);
7967
+ else parts.push("Path=/");
7968
+ if (Number.isFinite(maxAgeSec) && maxAgeSec > 0) parts.push(`Max-Age=${Math.floor(maxAgeSec)}`);
7969
+ if (a.domain) parts.push(`Domain=${a.domain}`);
7970
+ if (a.sameSite) {
7971
+ const ss = String(a.sameSite);
7972
+ parts.push(`SameSite=${ss.charAt(0).toUpperCase() + ss.slice(1)}`);
7973
+ } else {
7974
+ parts.push("SameSite=Lax");
7975
+ }
7976
+ if (a.secure) parts.push("Secure");
7977
+ if (a.httpOnly !== false) parts.push("HttpOnly");
7978
+ if (a.partitioned) parts.push("Partitioned");
7979
+ return parts.join("; ");
7980
+ }
7855
7981
  function pickHandler(svc) {
7856
7982
  if (!svc) return void 0;
7857
7983
  if (typeof svc.handleRequest === "function") return svc.handleRequest.bind(svc);
@@ -7945,6 +8071,115 @@ var AuthProxyPlugin = class {
7945
8071
  return c.json({ hasOwner: true });
7946
8072
  }
7947
8073
  }
8074
+ if (c.req.method === "POST" && subPath === "sso-handoff-issue") {
8075
+ try {
8076
+ const expected = (process.env.OS_CLOUD_API_KEY ?? "").trim();
8077
+ if (!expected) {
8078
+ return c.json({ error: "sso_handoff_disabled", reason: "OS_CLOUD_API_KEY unset on env runtime" }, 503);
8079
+ }
8080
+ const authz = c.req.header("authorization") ?? "";
8081
+ const provided = authz.toLowerCase().startsWith("bearer ") ? authz.slice(7).trim() : "";
8082
+ if (!provided || provided !== expected) {
8083
+ return c.json({ error: "unauthorized" }, 401);
8084
+ }
8085
+ if (typeof authSvc?.getAuthContext !== "function") {
8086
+ return c.json({ error: "auth_service_unavailable" }, 503);
8087
+ }
8088
+ const handoffAuthCtx = await authSvc.getAuthContext();
8089
+ const internal = handoffAuthCtx?.internalAdapter;
8090
+ if (!internal?.createVerificationValue) {
8091
+ return c.json({ error: "verification_api_unavailable" }, 503);
8092
+ }
8093
+ let body = {};
8094
+ try {
8095
+ body = await c.req.json();
8096
+ } catch {
8097
+ body = {};
8098
+ }
8099
+ const email = String(body?.email ?? "").toLowerCase().trim();
8100
+ if (!email) return c.json({ error: "email_required" }, 400);
8101
+ const name = body?.name == null ? null : String(body.name);
8102
+ const by = body?.by == null ? "service" : String(body.by);
8103
+ const envIdInBody = body?.envId == null ? null : String(body.envId);
8104
+ const handoff = (0, import_node_crypto3.randomUUID)().replace(/-/g, "") + (0, import_node_crypto3.randomUUID)().replace(/-/g, "");
8105
+ const ttlSec = 60;
8106
+ const expiresAt = new Date(Date.now() + ttlSec * 1e3);
8107
+ await internal.createVerificationValue({
8108
+ identifier: `sso-handoff:${handoff}`,
8109
+ value: JSON.stringify({ email, name, by, envId: envIdInBody ?? environmentId }),
8110
+ expiresAt
8111
+ });
8112
+ return c.json({
8113
+ token: handoff,
8114
+ expiresAt: expiresAt.toISOString(),
8115
+ ttlSec
8116
+ });
8117
+ } catch (err) {
8118
+ ctx.logger?.error?.("[AuthProxyPlugin] sso-handoff-issue failed", err instanceof Error ? err : new Error(String(err)));
8119
+ return c.json({ error: "sso_handoff_issue_failed", message: String(err?.message ?? err) }, 500);
8120
+ }
8121
+ }
8122
+ if (c.req.method === "GET" && subPath === "sso-exchange") {
8123
+ try {
8124
+ const token = (url.searchParams.get("token") ?? "").trim();
8125
+ const nextRaw = url.searchParams.get("next") ?? "/";
8126
+ const next = nextRaw.startsWith("/") ? nextRaw : "/";
8127
+ if (!token) return c.text("missing token", 400);
8128
+ if (typeof authSvc?.getAuthContext !== "function") {
8129
+ return c.text("auth service unavailable", 503);
8130
+ }
8131
+ const authCtx = await authSvc.getAuthContext();
8132
+ const internal = authCtx?.internalAdapter;
8133
+ if (!internal?.consumeVerificationValue) {
8134
+ return c.text("verification API unavailable", 503);
8135
+ }
8136
+ const consumed = await internal.consumeVerificationValue(`sso-handoff:${token}`);
8137
+ if (!consumed) return c.text("invalid or expired token", 401);
8138
+ const expiresAt = consumed?.expiresAt ? new Date(consumed.expiresAt).getTime() : 0;
8139
+ if (!expiresAt || expiresAt < Date.now()) return c.text("expired token", 401);
8140
+ let payload = {};
8141
+ try {
8142
+ payload = JSON.parse(String(consumed.value));
8143
+ } catch {
8144
+ payload = { email: String(consumed.value) };
8145
+ }
8146
+ const email = String(payload.email ?? "").toLowerCase().trim();
8147
+ if (!email) return c.text("handoff missing email", 400);
8148
+ const found = await internal.findUserByEmail(email, { includeAccounts: true });
8149
+ let userId = found?.user?.id;
8150
+ let hasCredentialAccount = (found?.accounts ?? []).some((a) => a.providerId === "credential" && a.password);
8151
+ if (!userId) {
8152
+ const created = await internal.createUser({
8153
+ email,
8154
+ name: payload.name ?? email,
8155
+ emailVerified: true
8156
+ });
8157
+ userId = created?.id;
8158
+ hasCredentialAccount = false;
8159
+ }
8160
+ if (!userId) return c.text("failed to provision user", 500);
8161
+ const session = await internal.createSession(userId, false);
8162
+ const rawToken = session?.token;
8163
+ const sessionExpiresAt = session?.expiresAt ? new Date(session.expiresAt) : new Date(Date.now() + 7 * 24 * 3600 * 1e3);
8164
+ if (!rawToken) return c.text("failed to mint session", 500);
8165
+ const secret = authCtx?.secret ?? "";
8166
+ if (!secret) return c.text("auth secret unavailable", 503);
8167
+ const cookieName = authCtx?.authCookies?.sessionToken?.name ?? "better-auth.session_token";
8168
+ const cookieAttrs = authCtx?.authCookies?.sessionToken?.attributes ?? {};
8169
+ const encoded = signSessionCookieValue(rawToken, secret);
8170
+ const maxAgeSec = Math.max(60, Math.floor((sessionExpiresAt.getTime() - Date.now()) / 1e3));
8171
+ const setCookie = buildSetCookieHeader(cookieName, encoded, cookieAttrs, maxAgeSec);
8172
+ const finalNext = hasCredentialAccount ? next : `/_console/system/profile?recovery_needed=true&next=${encodeURIComponent(next)}`;
8173
+ const headers = new Headers();
8174
+ headers.set("Set-Cookie", setCookie);
8175
+ headers.set("Location", finalNext);
8176
+ headers.set("Cache-Control", "no-store");
8177
+ return new Response(null, { status: 302, headers });
8178
+ } catch (err) {
8179
+ ctx.logger?.error?.("[AuthProxyPlugin] sso-exchange failed", err instanceof Error ? err : new Error(String(err)));
8180
+ return c.text(`sso-exchange failed: ${err?.message ?? String(err)}`, 500);
8181
+ }
8182
+ }
7948
8183
  const fn = await resolveAuthHandler(authSvc);
7949
8184
  if (!fn) {
7950
8185
  return c.json({ error: "auth_service_unavailable", environmentId }, 503);
@@ -8127,20 +8362,46 @@ var RuntimeConfigPlugin = class {
8127
8362
  return;
8128
8363
  }
8129
8364
  const rawApp = httpServer.getRawApp();
8130
- const payload = {
8131
- cloudUrl: this.cloudUrl,
8132
- singleEnvironment: this.singleEnvironment,
8133
- features: {
8134
- installLocal: this.installLocal,
8135
- marketplace: true
8365
+ const features = {
8366
+ installLocal: this.installLocal,
8367
+ marketplace: true
8368
+ };
8369
+ let envRegistry = null;
8370
+ try {
8371
+ envRegistry = ctx.getService("env-registry");
8372
+ } catch {
8373
+ }
8374
+ const handler = async (c) => {
8375
+ const rawHost = c.req.header("host") ?? "";
8376
+ const host = rawHost.split(":")[0].toLowerCase().trim();
8377
+ let defaultEnvironmentId;
8378
+ let defaultOrgId;
8379
+ let resolvedSingleEnv = this.singleEnvironment;
8380
+ if (envRegistry && host && typeof envRegistry.resolveHostname === "function") {
8381
+ try {
8382
+ const resolved = await envRegistry.resolveHostname(host);
8383
+ if (resolved?.environmentId) {
8384
+ defaultEnvironmentId = resolved.environmentId;
8385
+ if (resolved.organizationId) defaultOrgId = String(resolved.organizationId);
8386
+ resolvedSingleEnv = true;
8387
+ }
8388
+ } catch {
8389
+ }
8136
8390
  }
8391
+ return c.json({
8392
+ cloudUrl: this.cloudUrl,
8393
+ singleEnvironment: resolvedSingleEnv,
8394
+ defaultOrgId,
8395
+ defaultEnvironmentId,
8396
+ features
8397
+ });
8137
8398
  };
8138
- const handler = (c) => c.json(payload);
8139
8399
  rawApp.get("/api/v1/runtime/config", handler);
8140
8400
  rawApp.get("/api/v1/studio/runtime-config", handler);
8141
8401
  ctx.logger?.info?.("[RuntimeConfigPlugin] mounted /api/v1/runtime/config", {
8142
8402
  cloudUrl: this.cloudUrl || "(empty)",
8143
- installLocal: this.installLocal
8403
+ installLocal: this.installLocal,
8404
+ perHostEnvResolution: !!envRegistry
8144
8405
  });
8145
8406
  });
8146
8407
  };
@@ -8341,7 +8602,21 @@ var ObjectOSEnvironmentPlugin = class {
8341
8602
  factory,
8342
8603
  maxSize: this.config.kernelCacheSize,
8343
8604
  ttlMs: this.config.kernelTtlMs,
8344
- logger: ctx.logger
8605
+ logger: ctx.logger,
8606
+ // Only the HTTP client exposes /freshness; file-mode (CLI dev)
8607
+ // has no upstream to probe.
8608
+ freshnessProbe: this.config.controlPlaneUrl === "file" ? void 0 : async (envId, builtAtMs) => {
8609
+ const fresh = await client.getFreshness(envId);
8610
+ if (!fresh) return false;
8611
+ const t = fresh.lastPublishedAt ? Date.parse(fresh.lastPublishedAt) : NaN;
8612
+ if (!Number.isFinite(t)) return false;
8613
+ if (t <= builtAtMs) return false;
8614
+ try {
8615
+ client.invalidate(envId);
8616
+ } catch {
8617
+ }
8618
+ return true;
8619
+ }
8345
8620
  });
8346
8621
  this.kernelManager = kernelManager;
8347
8622
  ctx.registerService("env-registry", envRegistry);
@@ -8942,6 +9217,7 @@ __reExport(index_exports, require("@objectstack/core"), module.exports);
8942
9217
  resolveDefaultArtifactPath,
8943
9218
  resolveErrorReporter,
8944
9219
  resolveMetrics,
9220
+ resolveObjectStackHome,
8945
9221
  resolveRequestId,
8946
9222
  seedPlatformSsoClient,
8947
9223
  ...require("@objectstack/core")